/*
 * Copyright 2019 ACINQ SAS
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package fr.acinq.eclair.wire.protocol

import fr.acinq.bitcoin.scalacompat.ByteVector32
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.eclair.payment.{Bolt11Invoice, Bolt12Invoice, PaymentBlindedRoute}
import fr.acinq.eclair.wire.protocol.BlindedRouteData.PaymentRelayData
import fr.acinq.eclair.wire.protocol.CommonCodecs._
import fr.acinq.eclair.wire.protocol.OnionRoutingCodecs.{ForbiddenTlv, InvalidTlvPayload, MissingRequiredTlv}
import fr.acinq.eclair.wire.protocol.TlvCodecs._
import fr.acinq.eclair.{CltvExpiry, EncodedNodeId, Features, MilliSatoshi, ShortChannelId, UInt64}
import scodec.bits.{BitVector, ByteVector}

/**
 * Created by t-bast on 05/07/2019.
 */

/*
We support multiple payment flows, each having different requirements for what the onions contain. The following is an
overview of the onion contents we support.

STANDARD PAYMENT (fully source-routed, single-part):

    a -------------> b --------------------------> c --------------------------> d ---------------------------> e
          +-----------------------+     +-----------------------+     +-----------------------+     +-----------------------+
          | amount_fwd: 1025 msat |     | amount_fwd: 1010 msat |     | amount_fwd: 1000 msat |     | amount_fwd: 1000 msat |
          | expiry: 600030        |     | expiry: 600012        |     | expiry: 600000        |     | expiry: 600000        |
          | channel_id: 1105      |     | channel_id: 561       |     | channel_id: 42        |     | secret: xyz (opt)     |
          |-----------------------|     |-----------------------|     |-----------------------|     +-----------------------+
          |     (encrypted)       |     |     (encrypted)       |     |     (encrypted)       |     |          EOF          |
          +-----------------------+     +-----------------------+     +-----------------------+     +-----------------------+

STANDARD MULTI-PART PAYMENT (fully source-routed, multi-part):

    a -------------> b --------------------------> c --------------------------> d ---------------------------> e
          +-----------------------+     +-----------------------+     +-----------------------+     +-------------------------+
          | amount_fwd: 1025 msat |     | amount_fwd: 1010 msat |     | amount_fwd: 1000 msat |     | amount_fwd: 1000 msat   |
          | expiry: 600030        |     | expiry: 600012        |     | expiry: 600000        |     | expiry: 600000          |
          | channel_id: 1105      |     | channel_id: 561       |     | channel_id: 42        |     | secret: xyz             |
          |-----------------------|     |-----------------------|     |-----------------------|     | total_amount: 1500 msat |
          |     (encrypted)       |     |     (encrypted)       |     |     (encrypted)       |     +-------------------------+
          +-----------------------+     +-----------------------+     +-----------------------+     |           EOF           |
                                                                                                    +-------------------------+

TRAMPOLINE PAYMENT (partially source-routed, multi-part):

    a -------------> b ---------------------------> t1 -----------------------------> t2 -------------------------------> e
          +----------------------+     +---------------------------+     +---------------------------+     +-----------------------------+
          | amount_fwd: 900 msat |     | amount_fwd: 900 msat      |     | amount_fwd: 750 msat      |     | amount_fwd: 1000 msat       |
          | expiry: 600112       |     | expiry: 600112            |     | expiry: 600042            |     | expiry: 600000              |
          | channel_id: 42       |     | secret: aaaaa             |     | secret: zzzzz             |     | secret: xxxxx               | <- randomly generated by t2 (NOT the invoice secret)
          |----------------------|     | total_amount: 1650 msat   |     | total_amount: 1600 msat   |     | total_amount: 1500 msat     | <- t2 is using multi-part to pay e, still 500 msat more to receive
          |     (encrypted)      |     | trampoline_onion:         |     | trampoline_onion:         |     | trampoline_onion:           |
          +----------------------+     | +-----------------------+ |     | +-----------------------+ |     | +-------------------------+ |
                                       | | amount_fwd: 1600 msat | |     | | amount_fwd: 1500 msat | |     | | amount_fwd: 1500 msat   | |
                                       | | expiry: 600042        | |     | | expiry: 600000        | |     | | expiry: 600000          | |
                                       | | node_id: t2           | |     | | node_id: e            | |     | | total_amount: 2500 msat | | <- may be bigger than amount_fwd in case the payment is split among multiple trampoline routes
                                       | +-----------------------+ |     | +-----------------------+ |     | | secret: yyyyy           | | <- invoice secret
                                       | |      (encrypted)      | |     | |      (encrypted)      | |     | +-------------------------+ |
                                       | +-----------------------+ |     | +-----------------------+ |     | |         EOF             | |
                                       +---------------------------+     +---------------------------+     | +-------------------------+ |
                                       |             EOF           |     |             EOF           |     +-----------------------------+
                                       +---------------------------+     +---------------------------+     |             EOF             |
                                                                                                           +-----------------------------+

Notes:
  - there may be two layers of multi-part: a may split the payment between multiple trampoline routes, and inside each
  trampoline route payments may be split into multiple parts.
  - when multi-part is used to reach trampoline nodes, the payment secret in the outer onion is NOT the invoice secret.
  We want only the recipient to receive the invoice payment secret. The payment secrets in outer onions are generated
  randomly by the sender to simply prevent next-to-last non-trampoline nodes from probing their position in the route or
  steal some fees.

TRAMPOLINE PAYMENT TO LEGACY RECIPIENT (the last trampoline node converts to a standard payment to the final recipient):

    a -------------> b ----------------------------> t1 -----------------------------> t2 ---------------------------------- -> e ---------------------------> f
         +-----------------------+     +---------------------------+     +---------------------------------+        +-----------------------+     +-------------------------+
         | amount_fwd: 1750 msat |     | amount_fwd: 1750 msat     |     | amount_fwd: 1600 msat           |        | amount_fwd: 1000 msat |     | amount_fwd: 1000 msat   |
         | expiry: 600112        |     | expiry: 600112            |     | expiry: 600042                  |        | expiry: 600000        |     | expiry: 600000          |
         | channel_id: 42        |     | secret: yyyyy             |     | secret: zzzzz                   |  +---->| channel_id: 42        |---->| secret: xyz             | <- invoice secret (omitted if not supported by invoice)
         +-----------------------+     | total_amount: 1750 msat   |     | total_amount: 1600 msat         |  |     +-----------------------+     | total_amount: 2500 msat | <- t2 is using multi-part to pay 1500 msat to f, for a total payment
         |     (encrypted)       |     | trampoline_onion:         |     | trampoline_onion:               |  |     |     (encrypted)       |     +-------------------------+    of 2500 msat split between multiple trampoline routes (omitted if
         +-----------------------+     | +-----------------------+ |     | +-----------------------------+ |  |     +-----------------------+     |           EOF           |    MPP not supported by invoice).
                                       | | amount_fwd: 1600 msat | |     | | amount_fwd: 1500 msat       | |  |                                   +-------------------------+    The remaining 1000 msat needed to reach the total 2500 msat have
                                       | | expiry: 600042        | |     | | expiry: 600000              | |--+                                                                  been sent via a completely separate trampoline route (not included
                                       | | node_id: t2           | |     | | total_amount: 2500 msat     | |  |     +-----------------------+     +-------------------------+    in this diagram).
                                       | +-----------------------+ |     | | secret: xyz                 | |  |     | amount_fwd: 500 msat  |     | amount_fwd: 500 msat    |
                                       | |      (encrypted)      | |     | | node_id: f                  | |  |     | expiry: 600000        |     | expiry: 600000          |
                                       | +-----------------------+ |     | | invoice_features: 0x0a      | |  +---->| channel_id: 43        |---->| secret: xyz             |
                                       +---------------------------+     | | invoice_routing_info: ..... | |        +-----------------------+     | total_amount: 2500 msat |
                                       |             EOF           |     | +-----------------------------+ |        |     (encrypted)       |     +-------------------------+
                                       +---------------------------+     | |      (encrypted)            | |        +-----------------------+     |           EOF           |
                                                                         | +-----------------------------+ |                                      +-------------------------+
                                                                         +---------------------------------+
                                                                         |             EOF                 |
                                                                         +---------------------------------+

Notes:
  - the last trampoline node learns the payment details (who the recipient is, the payment amount and secret)
  - but it doesn't learn the sender's identity
  - if the invoice doesn't specify an amount, the last trampoline node can pay a lower amount than what the sender
  intended, thus stealing a lot of fees for himself (the wallet should disable paying to a 0-value invoice via
  trampoline to prevent this attack)
  - if f doesn't support MPP, t2 will send a single-part payment or fail if there isn't enough capacity
  - as in normal trampoline scenario, payment secrets in the outer onion are NOT the invoice secret

*/

/** Tlv types used inside a payment onion. */
sealed trait OnionPaymentPayloadTlv extends Tlv

object OnionPaymentPayloadTlv {

  /** Amount to forward to the next node. */
  case class AmountToForward(amount: MilliSatoshi) extends OnionPaymentPayloadTlv

  /** CLTV value to use for the HTLC offered to the next node. */
  case class OutgoingCltv(cltv: CltvExpiry) extends OnionPaymentPayloadTlv

  /** Id of the channel to use to forward a payment to the next node. */
  case class OutgoingChannelId(shortChannelId: ShortChannelId) extends OnionPaymentPayloadTlv

  /**
   * Bolt 11 payment details (only included for the last node).
   *
   * @param secret      payment secret specified in the Bolt 11 invoice.
   * @param totalAmount total amount in multi-part payments. When missing, assumed to be equal to AmountToForward.
   */
  case class PaymentData(secret: ByteVector32, totalAmount: MilliSatoshi) extends OnionPaymentPayloadTlv

  /** Id of the next node. */
  case class OutgoingNodeId(nodeId: PublicKey) extends OnionPaymentPayloadTlv

  /**
   * Route blinding lets the recipient provide some encrypted data for each intermediate node in the blinded part of the
   * route. This data cannot be decrypted or modified by the sender and usually contains information to locate the next
   * node without revealing it to the sender.
   */
  case class EncryptedRecipientData(data: ByteVector) extends OnionPaymentPayloadTlv

  /** Path key for the introduction node of a blinded route. */
  case class PathKey(publicKey: PublicKey) extends OnionPaymentPayloadTlv

  /** Total amount in blinded multi-part payments. */
  case class TotalAmount(totalAmount: MilliSatoshi) extends OnionPaymentPayloadTlv

  /**
   * When payment metadata is included in a Bolt 11 invoice, we should send it as-is to the recipient.
   * This lets recipients generate invoices without having to store anything on their side until the invoice is paid.
   */
  case class PaymentMetadata(data: ByteVector) extends OnionPaymentPayloadTlv

  /**
   * Invoice feature bits. Only included for intermediate trampoline nodes when they should convert to a legacy payment
   * because the final recipient doesn't support trampoline.
   */
  case class InvoiceFeatures(features: ByteVector) extends OnionPaymentPayloadTlv

  /**
   * Invoice routing hints. Only included for intermediate trampoline nodes when they should convert to a legacy payment
   * because the final recipient doesn't support trampoline.
   */
  case class InvoiceRoutingInfo(extraHops: List[List[Bolt11Invoice.ExtraHop]]) extends OnionPaymentPayloadTlv

  /** An encrypted trampoline onion packet. */
  case class TrampolineOnion(packet: OnionRoutingPacket) extends OnionPaymentPayloadTlv

  /** Pre-image included by the sender of a payment in case of a donation */
  case class KeySend(paymentPreimage: ByteVector32) extends OnionPaymentPayloadTlv

  /** Only included for intermediate trampoline nodes that should wait before forwarding this payment */
  case class AsyncPayment() extends OnionPaymentPayloadTlv

  /** Blinded paths to relay the payment to */
  case class OutgoingBlindedPaths(paths: Seq[PaymentBlindedRoute]) extends OnionPaymentPayloadTlv
}

object PaymentOnion {

  import OnionPaymentPayloadTlv._

  /*
   *                                                    PerHopPayload
   *                                                          |
   *                                                          |
   *                        +---------------------------------+-----------------------------+
   *                        |                                 |                             |
   *                        |                                 |                             |
   *               IntermediatePayload                   FinalPayload          OutgoingBlindedPerHopPayload
   *                        |                                 |
   *                        |                                 |
   *           +------------+-------------+             +------+------+
   *           |                          |             |             |
   *           |                          |             |             |
   *     ChannelRelay                 NodeRelay      Standard      Blinded
   *           |                          |
   *           |                          |
   *    +------+------+            +----------------+
   *    |             |            |                |
   *    |             |            |                |
   * Standard      Blinded      Standard      ToBlindedPaths
   */

  /** Per-hop payload from an HTLC's payment onion (after decryption and decoding). */
  sealed trait PerHopPayload {
    def records: TlvStream[OnionPaymentPayloadTlv]
  }

  /** Per-hop payload for an intermediate node. */
  sealed trait IntermediatePayload extends PerHopPayload

  object IntermediatePayload {
    sealed trait ChannelRelay extends IntermediatePayload {
      // @formatter:off
      /** The outgoing channel, or the nodeId of one of our peers. */
      def outgoing: Either[EncodedNodeId.WithPublicKey, ShortChannelId]
      def amountToForward(incomingAmount: MilliSatoshi): MilliSatoshi
      def outgoingCltv(incomingCltv: CltvExpiry): CltvExpiry
      // @formatter:on
    }

    object ChannelRelay {
      case class Standard(records: TlvStream[OnionPaymentPayloadTlv]) extends ChannelRelay {
        // @formatter:off
        val amountOut = records.get[AmountToForward].get.amount
        val cltvOut = records.get[OutgoingCltv].get.cltv
        override val outgoing = Right(records.get[OutgoingChannelId].get.shortChannelId)
        override def amountToForward(incomingAmount: MilliSatoshi): MilliSatoshi = amountOut
        override def outgoingCltv(incomingCltv: CltvExpiry): CltvExpiry = cltvOut
        // @formatter:on
      }

      object Standard {
        def apply(outgoingChannelId: ShortChannelId, amountToForward: MilliSatoshi, outgoingCltv: CltvExpiry): Standard = {
          Standard(TlvStream(OnionPaymentPayloadTlv.AmountToForward(amountToForward), OnionPaymentPayloadTlv.OutgoingCltv(outgoingCltv), OnionPaymentPayloadTlv.OutgoingChannelId(outgoingChannelId)))
        }

        def validate(records: TlvStream[OnionPaymentPayloadTlv]): Either[InvalidTlvPayload, Standard] = {
          if (records.get[AmountToForward].isEmpty) return Left(MissingRequiredTlv(UInt64(2)))
          if (records.get[OutgoingCltv].isEmpty) return Left(MissingRequiredTlv(UInt64(4)))
          if (records.get[OutgoingChannelId].isEmpty) return Left(MissingRequiredTlv(UInt64(6)))
          Right(Standard(records))
        }
      }

      /**
       * @param paymentRelayData decrypted relaying data from the encrypted_recipient_data tlv.
       * @param nextPathKey      path key that must be forwarded to the next hop.
       */
      case class Blinded(records: TlvStream[OnionPaymentPayloadTlv], paymentRelayData: PaymentRelayData, nextPathKey: PublicKey) extends ChannelRelay {
        // @formatter:off
        override val outgoing = paymentRelayData.outgoing
        override def amountToForward(incomingAmount: MilliSatoshi): MilliSatoshi = paymentRelayData.amountToForward(incomingAmount)
        override def outgoingCltv(incomingCltv: CltvExpiry): CltvExpiry = paymentRelayData.outgoingCltv(incomingCltv)
        // @formatter:on
      }

      object Blinded {
        def validate(records: TlvStream[OnionPaymentPayloadTlv], blindedRecords: TlvStream[RouteBlindingEncryptedDataTlv], nextPathKey: PublicKey): Either[InvalidTlvPayload, Blinded] = {
          if (records.get[EncryptedRecipientData].isEmpty) return Left(MissingRequiredTlv(UInt64(10)))
          // Bolt 4: MUST return an error if the payload contains other tlv fields than `encrypted_recipient_data` and `current_path_key`.
          if (records.unknown.nonEmpty) return Left(ForbiddenTlv(records.unknown.head.tag))
          records.records.find {
            case _: EncryptedRecipientData => false
            case _: PathKey => false
            case _ => true
          } match {
            case Some(_) => return Left(ForbiddenTlv(UInt64(0)))
            case None => // no forbidden tlv found
          }
          BlindedRouteData.validatePaymentRelayData(blindedRecords).map(paymentRelayData => Blinded(records, paymentRelayData, nextPathKey))
        }
      }
    }

    sealed trait NodeRelay extends IntermediatePayload {
      // @formatter:off
      def outgoingAmount(incomingAmount: MilliSatoshi): MilliSatoshi
      def outgoingExpiry(incomingCltv: CltvExpiry): CltvExpiry
      // @formatter:on
    }

    object NodeRelay {
      case class Standard(records: TlvStream[OnionPaymentPayloadTlv]) extends NodeRelay {
        val amountToForward = records.get[AmountToForward].get.amount
        val outgoingCltv = records.get[OutgoingCltv].get.cltv
        val outgoingNodeId = records.get[OutgoingNodeId].get.nodeId
        val isAsyncPayment: Boolean = records.get[AsyncPayment].isDefined

        // @formatter:off
        override def outgoingAmount(incomingAmount: MilliSatoshi): MilliSatoshi = amountToForward
        override def outgoingExpiry(incomingCltv: CltvExpiry): CltvExpiry = outgoingCltv
        // @formatter:on
      }

      object Standard {
        def apply(amount: MilliSatoshi, expiry: CltvExpiry, nextNodeId: PublicKey): Standard = {
          Standard(TlvStream(AmountToForward(amount), OutgoingCltv(expiry), OutgoingNodeId(nextNodeId)))
        }

        def validate(records: TlvStream[OnionPaymentPayloadTlv]): Either[InvalidTlvPayload, Standard] = {
          if (records.get[AmountToForward].isEmpty) return Left(MissingRequiredTlv(UInt64(2)))
          if (records.get[OutgoingCltv].isEmpty) return Left(MissingRequiredTlv(UInt64(4)))
          if (records.get[OutgoingNodeId].isEmpty) return Left(MissingRequiredTlv(UInt64(66098)))
          if (records.get[EncryptedRecipientData].nonEmpty) return Left(ForbiddenTlv(UInt64(10)))
          if (records.get[PathKey].nonEmpty) return Left(ForbiddenTlv(UInt64(12)))
          Right(Standard(records))
        }

        /** Create a standard trampoline inner payload instructing the trampoline node to wait for a trigger before sending an async payment. */
        def createNodeRelayForAsyncPayment(amount: MilliSatoshi, expiry: CltvExpiry, nextNodeId: PublicKey): Standard = {
          Standard(TlvStream(AmountToForward(amount), OutgoingCltv(expiry), OutgoingNodeId(nextNodeId), AsyncPayment()))
        }
      }

      /** We relay to a payment recipient that doesn't support trampoline, which exposes its identity. */
      case class ToNonTrampoline(records: TlvStream[OnionPaymentPayloadTlv]) extends NodeRelay {
        val amountToForward = records.get[AmountToForward].get.amount
        val outgoingCltv = records.get[OutgoingCltv].get.cltv
        val outgoingNodeId = records.get[OutgoingNodeId].get.nodeId
        val totalAmount = records.get[PaymentData].map(_.totalAmount match {
          case MilliSatoshi(0) => amountToForward
          case totalAmount => totalAmount
        }).get
        val paymentSecret = records.get[PaymentData].map(_.secret).get
        val paymentMetadata = records.get[PaymentMetadata].map(_.data)
        val invoiceFeatures = records.get[InvoiceFeatures].map(_.features).getOrElse(ByteVector.empty)
        val invoiceRoutingInfo = records.get[InvoiceRoutingInfo].map(_.extraHops).get

        // @formatter:off
        override def outgoingAmount(incomingAmount: MilliSatoshi): MilliSatoshi = amountToForward
        override def outgoingExpiry(incomingCltv: CltvExpiry): CltvExpiry = outgoingCltv
        // @formatter:on
      }

      object ToNonTrampoline {
        def apply(amount: MilliSatoshi, totalAmount: MilliSatoshi, expiry: CltvExpiry, targetNodeId: PublicKey, invoice: Bolt11Invoice): ToNonTrampoline = {
          val tlvs: Set[OnionPaymentPayloadTlv] = Set(
            Some(AmountToForward(amount)),
            Some(OutgoingCltv(expiry)),
            Some(PaymentData(invoice.paymentSecret, totalAmount)),
            invoice.paymentMetadata.map(m => PaymentMetadata(m)),
            Some(OutgoingNodeId(targetNodeId)),
            Some(InvoiceFeatures(invoice.features.toByteVector)),
            Some(InvoiceRoutingInfo(invoice.routingInfo.toList.map(_.toList)))
          ).flatten
          ToNonTrampoline(TlvStream(tlvs))
        }

        def validate(records: TlvStream[OnionPaymentPayloadTlv]): Either[InvalidTlvPayload, ToNonTrampoline] = {
          if (records.get[AmountToForward].isEmpty) return Left(MissingRequiredTlv(UInt64(2)))
          if (records.get[OutgoingCltv].isEmpty) return Left(MissingRequiredTlv(UInt64(4)))
          if (records.get[PaymentData].isEmpty) return Left(MissingRequiredTlv(UInt64(8)))
          if (records.get[OutgoingNodeId].isEmpty) return Left(MissingRequiredTlv(UInt64(66098)))
          if (records.get[InvoiceRoutingInfo].isEmpty) return Left(MissingRequiredTlv(UInt64(66099)))
          if (records.get[EncryptedRecipientData].nonEmpty) return Left(ForbiddenTlv(UInt64(10)))
          if (records.get[PathKey].nonEmpty) return Left(ForbiddenTlv(UInt64(12)))
          Right(ToNonTrampoline(records))
        }
      }

      /** We relay to a payment recipient that doesn't support trampoline, but hides its identity using blinded paths. */
      case class ToBlindedPaths(records: TlvStream[OnionPaymentPayloadTlv]) extends NodeRelay {
        val amountToForward = records.get[AmountToForward].get.amount
        val outgoingCltv = records.get[OutgoingCltv].get.cltv
        val outgoingBlindedPaths = records.get[OutgoingBlindedPaths].get.paths
        val invoiceFeatures = records.get[InvoiceFeatures].get.features

        // @formatter:off
        override def outgoingAmount(incomingAmount: MilliSatoshi): MilliSatoshi = amountToForward
        override def outgoingExpiry(incomingCltv: CltvExpiry): CltvExpiry = outgoingCltv
        // @formatter:on
      }

      object ToBlindedPaths {
        def apply(amount: MilliSatoshi, expiry: CltvExpiry, invoice: Bolt12Invoice): ToBlindedPaths = {
          val tlvs: Set[OnionPaymentPayloadTlv] = Set(
            Some(AmountToForward(amount)),
            Some(OutgoingCltv(expiry)),
            Some(OutgoingBlindedPaths(invoice.blindedPaths)),
            Some(InvoiceFeatures(invoice.features.toByteVector)),
          ).flatten
          ToBlindedPaths(TlvStream(tlvs))
        }

        def validate(records: TlvStream[OnionPaymentPayloadTlv]): Either[InvalidTlvPayload, ToBlindedPaths] = {
          if (records.get[AmountToForward].isEmpty) return Left(MissingRequiredTlv(UInt64(2)))
          if (records.get[OutgoingCltv].isEmpty) return Left(MissingRequiredTlv(UInt64(4)))
          if (records.get[OutgoingBlindedPaths].isEmpty) return Left(MissingRequiredTlv(UInt64(66102)))
          if (records.get[InvoiceFeatures].isEmpty) return Left(MissingRequiredTlv(UInt64(66097)))
          if (records.get[EncryptedRecipientData].nonEmpty) return Left(ForbiddenTlv(UInt64(10)))
          if (records.get[PathKey].nonEmpty) return Left(ForbiddenTlv(UInt64(12)))
          Right(ToBlindedPaths(records))
        }
      }
    }
  }

  /** Per-hop payload for the final recipient. */
  sealed trait FinalPayload extends PerHopPayload {
    // @formatter:off
    def amount: MilliSatoshi
    def totalAmount: MilliSatoshi
    def expiry: CltvExpiry
    // @formatter:on
  }

  object FinalPayload {
    case class Standard(records: TlvStream[OnionPaymentPayloadTlv]) extends FinalPayload {
      override val amount = records.get[AmountToForward].get.amount
      override val totalAmount = records.get[PaymentData].map(_.totalAmount match {
        case MilliSatoshi(0) => amount
        case totalAmount => totalAmount
      }).getOrElse(amount)
      override val expiry = records.get[OutgoingCltv].get.cltv
      val paymentSecret = records.get[PaymentData].map(_.secret).orElse(records.get[KeySend].map(_.paymentPreimage)).get
      val paymentPreimage = records.get[KeySend].map(_.paymentPreimage)
      val paymentMetadata = records.get[PaymentMetadata].map(_.data)
      val isTrampoline = records.get[TrampolineOnion].nonEmpty
    }

    object Standard {
      def validate(records: TlvStream[OnionPaymentPayloadTlv]): Either[InvalidTlvPayload, Standard] = {
        if (records.get[AmountToForward].isEmpty) return Left(MissingRequiredTlv(UInt64(2)))
        if (records.get[OutgoingCltv].isEmpty) return Left(MissingRequiredTlv(UInt64(4)))
        // KeySend payments don't provide a payment secret, but they don't need one: the preimage act as secret.
        if (records.get[PaymentData].isEmpty && records.get[KeySend].isEmpty) return Left(MissingRequiredTlv(UInt64(8)))
        Right(Standard(records))
      }

      def createPayload(amount: MilliSatoshi, totalAmount: MilliSatoshi, expiry: CltvExpiry, paymentSecret: ByteVector32, paymentMetadata: Option[ByteVector] = None, trampolineOnion_opt: Option[OnionRoutingPacket] = None, customTlvs: Set[GenericTlv] = Set.empty): Standard = {
        val tlvs: Set[OnionPaymentPayloadTlv] = Set(
          Some(AmountToForward(amount)),
          Some(OutgoingCltv(expiry)),
          Some(PaymentData(paymentSecret, totalAmount)),
          paymentMetadata.map(m => PaymentMetadata(m)),
          trampolineOnion_opt.map(o => TrampolineOnion(o)),
        ).flatten
        Standard(TlvStream(tlvs, customTlvs))
      }

      def createKeySendPayload(amount: MilliSatoshi, expiry: CltvExpiry, preimage: ByteVector32, customTlvs: Set[GenericTlv] = Set.empty): Standard = {
        val tlvs: Set[OnionPaymentPayloadTlv] = Set(
          AmountToForward(amount),
          OutgoingCltv(expiry),
          KeySend(preimage)
        )
        Standard(TlvStream(tlvs, customTlvs))
      }

      /** Create a trampoline outer payload. */
      def createTrampolinePayload(amount: MilliSatoshi, totalAmount: MilliSatoshi, expiry: CltvExpiry, paymentSecret: ByteVector32, trampolinePacket: OnionRoutingPacket): Standard = {
        Standard(TlvStream(AmountToForward(amount), OutgoingCltv(expiry), PaymentData(paymentSecret, totalAmount), TrampolineOnion(trampolinePacket)))
      }
    }

    /**
     * @param blindedRecords decrypted tlv stream from the encrypted_recipient_data tlv.
     */
    case class Blinded(records: TlvStream[OnionPaymentPayloadTlv], blindedRecords: TlvStream[RouteBlindingEncryptedDataTlv]) extends FinalPayload {
      override val amount = records.get[AmountToForward].get.amount
      override val totalAmount = records.get[TotalAmount].map(_.totalAmount).getOrElse(amount)
      override val expiry = records.get[OutgoingCltv].get.cltv
      val pathKey_opt: Option[PublicKey] = records.get[PathKey].map(_.publicKey)
      val pathId = blindedRecords.get[RouteBlindingEncryptedDataTlv.PathId].get.data
      val paymentConstraints_opt = blindedRecords.get[RouteBlindingEncryptedDataTlv.PaymentConstraints]
      val allowedFeatures = blindedRecords.get[RouteBlindingEncryptedDataTlv.AllowedFeatures].map(_.features).getOrElse(Features.empty)
    }

    object Blinded {
      def validate(records: TlvStream[OnionPaymentPayloadTlv], blindedRecords: TlvStream[RouteBlindingEncryptedDataTlv]): Either[InvalidTlvPayload, Blinded] = {
        if (records.get[AmountToForward].isEmpty) return Left(MissingRequiredTlv(UInt64(2)))
        if (records.get[OutgoingCltv].isEmpty) return Left(MissingRequiredTlv(UInt64(4)))
        if (records.get[EncryptedRecipientData].isEmpty) return Left(MissingRequiredTlv(UInt64(10)))
        // Bolt 4: MUST return an error if the payload contains other tlv fields than `encrypted_recipient_data`, `current_path_key`, `amt_to_forward`, `outgoing_cltv_value` and `total_amount_msat`.
        if (records.unknown.nonEmpty) return Left(ForbiddenTlv(records.unknown.head.tag))
        records.records.find {
          case _: AmountToForward => false
          case _: OutgoingCltv => false
          case _: EncryptedRecipientData => false
          case _: PathKey => false
          case _: TotalAmount => false
          case _ => true
        } match {
          case Some(_) => return Left(ForbiddenTlv(UInt64(0)))
          case None => // no forbidden tlv found
        }
        BlindedRouteData.validPaymentRecipientData(blindedRecords).map(blindedRecords => Blinded(records, blindedRecords))
      }
    }
  }

  /**
   * An opaque blinded payload (used when sending to a blinded route, never used to decode incoming payloads).
   * We cannot use the other payload types because we cannot decrypt the recipient encrypted data, so we don't even
   * know if those payloads are valid.
   */
  case class OutgoingBlindedPerHopPayload(records: TlvStream[OnionPaymentPayloadTlv]) extends PerHopPayload {
    require(records.get[EncryptedRecipientData].nonEmpty, "blinded per-hop payload must contain encrypted data")
  }

  object OutgoingBlindedPerHopPayload {
    def createIntroductionPayload(encryptedRecipientData: ByteVector, pathKey: PublicKey): OutgoingBlindedPerHopPayload = {
      OutgoingBlindedPerHopPayload(TlvStream(EncryptedRecipientData(encryptedRecipientData), PathKey(pathKey)))
    }

    def createIntermediatePayload(encryptedRecipientData: ByteVector): OutgoingBlindedPerHopPayload = {
      OutgoingBlindedPerHopPayload(TlvStream(EncryptedRecipientData(encryptedRecipientData)))
    }

    def createFinalPayload(amount: MilliSatoshi, totalAmount: MilliSatoshi, expiry: CltvExpiry, encryptedRecipientData: ByteVector, customTlvs: Set[GenericTlv] = Set.empty): OutgoingBlindedPerHopPayload = {
      OutgoingBlindedPerHopPayload(TlvStream(Set[OnionPaymentPayloadTlv](AmountToForward(amount), TotalAmount(totalAmount), OutgoingCltv(expiry), EncryptedRecipientData(encryptedRecipientData)), customTlvs))
    }

    def createFinalIntroductionPayload(amount: MilliSatoshi, totalAmount: MilliSatoshi, expiry: CltvExpiry, pathKey: PublicKey, encryptedRecipientData: ByteVector, customTlvs: Set[GenericTlv] = Set.empty): OutgoingBlindedPerHopPayload = {
      OutgoingBlindedPerHopPayload(TlvStream(Set[OnionPaymentPayloadTlv](AmountToForward(amount), TotalAmount(totalAmount), OutgoingCltv(expiry), EncryptedRecipientData(encryptedRecipientData), PathKey(pathKey)), customTlvs))
    }
  }

  /**
   * A payload used when creating test payments to a first trampoline node without using MPP, since the spec allows it.
   * We never use this class to decode incoming payments though: we will instead insert a dummy [[PaymentData]] entry
   * when decrypting the onion to avoid duplicating code and data. This class is thus only used to allow creating such
   * payloads.
   */
  case class TrampolineWithoutMppPayload(records: TlvStream[OnionPaymentPayloadTlv]) extends PerHopPayload {
    require(records.get[TrampolineOnion].nonEmpty, "trampoline onion must be included")
  }

  object TrampolineWithoutMppPayload {
    def create(amount: MilliSatoshi, expiry: CltvExpiry, trampolinePacket: OnionRoutingPacket): TrampolineWithoutMppPayload = {
      TrampolineWithoutMppPayload(TlvStream(AmountToForward(amount), OutgoingCltv(expiry), TrampolineOnion(trampolinePacket)))
    }
  }

}

object PaymentOnionCodecs {

  import OnionPaymentPayloadTlv._
  import scodec.codecs._
  import scodec.{Codec, DecodeResult, Decoder}

  val paymentOnionPayloadLength = 1300
  val paymentOnionPacketCodec: Codec[OnionRoutingPacket] = OnionRoutingCodecs.onionRoutingPacketCodec(paymentOnionPayloadLength)

  /**
   * The 1.1 BOLT spec changed the payment onion frame format to use variable-length per-hop payloads.
   * The first bytes contain a varint encoding the length of the payload data (not including the trailing mac).
   * That varint is considered to be part of the payload, so the payload length includes the number of bytes used by
   * the varint prefix.
   */
  val payloadLengthDecoder = Decoder[Long]((bits: BitVector) =>
    varintoverflow.decode(bits).map(d => DecodeResult(d.value + (bits.length - d.remainder.length) / 8, d.remainder)))

  private val amountToForward: Codec[AmountToForward] = tlvField(tmillisatoshi)

  private val outgoingCltv: Codec[OutgoingCltv] = tlvField(tu32.xmap(cltv => OutgoingCltv(CltvExpiry(cltv)), (c: OutgoingCltv) => c.cltv.toLong))

  private val outgoingChannelId: Codec[OutgoingChannelId] = tlvField(shortchannelid)

  private val paymentData: Codec[PaymentData] = tlvField(("payment_secret" | bytes32) :: ("total_msat" | tmillisatoshi))

  private val encryptedRecipientData: Codec[EncryptedRecipientData] = tlvField(bytes)

  private val pathKey: Codec[PathKey] = fixedLengthTlvField(33, publicKey)

  private val outgoingNodeId: Codec[OutgoingNodeId] = fixedLengthTlvField(33, publicKey)

  private val paymentMetadata: Codec[PaymentMetadata] = tlvField(bytes)

  private val totalAmount: Codec[TotalAmount] = tlvField(tmillisatoshi)

  private val invoiceFeatures: Codec[InvoiceFeatures] = tlvField(bytes)

  private val invoiceRoutingInfo: Codec[InvoiceRoutingInfo] = tlvField(list(listOfN(uint8, Bolt11Invoice.Codecs.extraHopCodec)))

  private val trampolineOnion: Codec[TrampolineOnion] = tlvField(OnionRoutingCodecs.variableSizeOnionRoutingPacketCodec)

  private val paymentBlindedRoute: Codec[PaymentBlindedRoute] =
    (("route" | OfferCodecs.blindedRouteCodec) ::
      ("paymentInfo" | OfferCodecs.paymentInfo)).as[PaymentBlindedRoute]

  private val outgoingBlindedPaths: Codec[OutgoingBlindedPaths] =
    tlvField(list(paymentBlindedRoute).xmap[Seq[PaymentBlindedRoute]](_.toSeq, _.toList))

  private val keySend: Codec[KeySend] = tlvField(bytes32)

  private val asyncPayment: Codec[AsyncPayment] = tlvField(provide(AsyncPayment()))

  private val onionTlvCodec = discriminated[OnionPaymentPayloadTlv].by(varint)
    .typecase(UInt64(2), amountToForward)
    .typecase(UInt64(4), outgoingCltv)
    .typecase(UInt64(6), outgoingChannelId)
    .typecase(UInt64(8), paymentData)
    .typecase(UInt64(10), encryptedRecipientData)
    .typecase(UInt64(12), pathKey)
    .typecase(UInt64(16), paymentMetadata)
    .typecase(UInt64(18), totalAmount)
    // Types below aren't specified - use cautiously when deploying (be careful with backwards-compatibility).
    .typecase(UInt64(66097), invoiceFeatures)
    .typecase(UInt64(66098), outgoingNodeId)
    .typecase(UInt64(66099), invoiceRoutingInfo)
    .typecase(UInt64(66100), trampolineOnion)
    .typecase(UInt64(66102), outgoingBlindedPaths)
    .typecase(UInt64(181324718L), asyncPayment)
    .typecase(UInt64(5482373484L), keySend)

  val perHopPayloadCodec: Codec[TlvStream[OnionPaymentPayloadTlv]] = catchAllCodec(TlvCodecs.lengthPrefixedTlvStream[OnionPaymentPayloadTlv](onionTlvCodec).complete)

}